/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.business;

import TimeRecording.business.calcs.DailyAllocation;
import TimeRecording.enums.PartOfDay;
import TimeRecording.enums.SicknessReportStatus;
import TimeRecording.tables.Employee;
import TimeRecording.tables.SicknessReport;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;

/**
 *
 * @author david
 */
public class SicknessCalculations {

    private PeriodCalculations periodCalcs = new PeriodCalculations();

    public SicknessCalculations() {
        super();
    }

   /**
     * This method will calculate the sickness spend for the supplied sickness.
     *
     * The class instance is designed to be reusable such that multiple sickness
     * instances can be thrown at it;  if sufficient data is present (a partial
     * "intelligent" lookup is performed when accessing the database) then no
     * back-end access will be performed.
     *
     * @return
     */
    public double getSicknessSpend(EntityManager entityManager,
            SicknessReport newSickness, SpendExplanation explanation) {

        // Prepare our explanation (if one supplied)
        if (explanation != null) {
            explanation.start();
        }

        // Take a bullet-proof copy of the the supplied sickness
        SicknessReport safeSickness = new SicknessReport();
        safeSickness.copyClipped(newSickness);

        periodCalcs.refreshData(entityManager,
                safeSickness.getEmployeeId(),
                safeSickness.getFirstDayOfSickness(),
                safeSickness.getLastDayOfSickness());

        periodCalcs.insertSicknessReport(safeSickness);

        // Reassess the clocked time, etc.
        periodCalcs.reassessWorkingDays(entityManager);

        double totalCost = 0.0;

        // Work out the sickness cost
        for (DailyAllocation day : periodCalcs.getDailyInformation()) {

            // Skip any days calculated that do not fall in the sickness
            if (day.getDate().before(safeSickness.getFirstDayOfSickness())) {
                continue;
            }
            if (day.getDate().after(safeSickness.getLastDayOfSickness())) {
                continue;
            }

            double cost = day.getTotalSicknessTime();

            PartOfDay partOfDayForThisSickness = PartOfDay.ALL_DAY;
            double divisor = 1.0;

            // For our explanation, we'll probably have
            // whole days except possibly at the start
            if (day.getDate().equals(safeSickness.getFirstDayOfSickness())) {
                partOfDayForThisSickness = safeSickness.getFirstDayPart() ;
            }

            // For our explanation, we'll probably have
            // whole days except possibly at the end
            if (day.getDate().equals(safeSickness.getLastDayOfSickness())) {
                partOfDayForThisSickness = safeSickness.getLastDayPart() ;
            }

            PartOfDay partOfDayForAllSickness = PartOfDay.add(
                    day.getSickness().getPartOfDay(),
                    day.getUnapprovedSickness().getPartOfDay());

            if (partOfDayForThisSickness != PartOfDay.ALL_DAY &&
                    partOfDayForAllSickness == PartOfDay.ALL_DAY){
                divisor = 2.0;
            }
            
            cost = cost / divisor;

            if (explanation != null) {
                explanation.add(day.getDate(), partOfDayForThisSickness, cost);
            }

            totalCost += cost;
        }

        return totalCost;
    }

    public static PartOfDay isSick(Employee employee, Date date) {

        PartOfDay partOfDaySick = null;

        for (SicknessReport report : employee.getSicknessReports()) {

            // Only include acknowledged sickness

            if (report.getStatus() != SicknessReportStatus.ACKNOWLEDGED &&
                    report.getStatus() != SicknessReportStatus.PROCESSED &&
                    report.getStatus() != SicknessReportStatus.ARCHIVED) {
                continue;
            }

            PartOfDay partOfDay = DaisyChainUtilities.getDayPart(
                    report.getFirstDayOfSickness(),
                    report.getLastDayOfSickness(),
                    report.getFirstDayPart(),
                    report.getLastDayPart(),
                    date);

            // Any sickness on that day?

            if (partOfDay != null) {

                // If we've already encountered sickness for that day then it
                // can only mean that two separate sickness records have been
                // entered: once for AM and one for PM (probbaly will never
                // happen but just in case...)

                if (partOfDaySick == null) {
                    partOfDaySick = partOfDay;
                } else {
                    partOfDaySick = PartOfDay.ALL_DAY;
                }
            }
        }

        return partOfDaySick;
    }

    public static boolean validateReport(SicknessReport newReport) {

        Employee newEmployee = newReport.getEmployeeId();
        Collection<SicknessReport> reports = newEmployee.getSicknessReports();

        int newReportId = 0;
        if (newReport.getReportId() != null) {
            newReportId = newReport.getReportId();
        }

        for (SicknessReport report : reports) {

            // Do not include the holiday itself when checking for a clash
            int reportId = report.getReportId();
            if (reportId == newReportId) {
                continue;
            }

            if (report.clashesWith(newReport)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Reassesses the spend on all sickness requests (note that the method
     * currently visits all records but could be tweaked to only visit those
     * that require re-assessment.  The transaction is managed outside of this
     * method.
     *
     * @param entityManager the entity manager through which database is updated
     */
    public void reassessSickness(EntityManager entityManager) {

        Query query = entityManager.createNamedQuery("SicknessReport.findAll");
        List<SicknessReport> reports = query.getResultList();

        for (SicknessReport report : reports) {

            double oldCost = report.getHoursLost();
            double newCost = getSicknessSpend(entityManager, report, null);

            // Persist any changes
            if (oldCost != newCost) {
                entityManager.merge(report);
            }
        }
    }

    /**
     * Update the running total for how much sickness has been taken.  Archived
     * and yet-to-be-submitted records are ignored.  Records not yet processed
     * (i.e. in progress) will have their spend re-assessed based on current
     * working patterns and, what is more likely, the end date of the sickness.
     * The nature of sickness would typically mean that a sickness report is
     * supplied retrospectively or else is open ended.
     *
     * @param employee
     * @return a flag indicating changes have been made (or not)
     */
    public boolean reassessSummary(EntityManager entityManager, Employee employee) {

        double oldSickness = employee.getSickness();
        double sickness = 0;

        // Set up details of our employee and the period for our calculations

        Date startOfyear = DaisyChainUtilities.firstDayOfSystemYear(null).getTime();
        Date endOfYear = DaisyChainUtilities.lastDayOfSystemYear(null).getTime();

        periodCalcs.refreshData(entityManager, employee,
                startOfyear, endOfYear);

        periodCalcs.reassessWorkingDays(entityManager);

        for (DailyAllocation day : periodCalcs.getDailyInformation()) {

            // Skip any days that are beyond the periphery of our date range
            if (day.getDate().before(startOfyear) || day.getDate().after(endOfYear)) {
                continue;
            }

            sickness += day.getSickness().getSpend();
        }

        // Only update the details if they've changed (the JPA stuff may well
        // perform unexpected/unnecessary processing under the covers)

        if (sickness != oldSickness) {
            employee.setSickness(sickness);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Reassess the sickness of all employees; it is envisaged that this method
     * will be called as part of end-of-day housekeeping.  Any exceptions are
     * passed back up to the calling method to be reported.  The transaction is
     * managed outside of this method.
     *
     * @param entityManager the entity manager through which database is updated
     */
    public void reassessSummaries(EntityManager entityManager) {

        // Get all emplyees

        List<Employee> employees = Employee.getEmployees(entityManager);

        for (Employee employee : employees) {

            // Persist any changes
            if (reassessSummary(entityManager, employee)) {
                entityManager.merge(employee);
            }
        }
    }

    /**
     * Carries out month end processing for sickness reports; this involves
     * identifying records that can be closed (i.e. ones that are in a submitted
     * or acknowledged state with a last day prior to the cut-off date).  These
     * sickness reports are given a final refresh before the status is set to
     * "processed" and merged back to the database.
     *
     * @param entityManager manages database updates
     * @param cutOffDate the date up to which sickness records are included
     */
    public void monthEndProcessing(EntityManager entityManager,
            Date cutOffDate) {

        Query query = entityManager.createNamedQuery("SicknessReport.findAll");
        List<SicknessReport> reports = query.getResultList();

        for (SicknessReport report : reports) {

            // We're only interested in acknowledged or submitted entries

            if (report.getStatus() != SicknessReportStatus.ACKNOWLEDGED &&
                    report.getStatus() != SicknessReportStatus.SUBMITTED) {
                continue;
            }

            // Now check to see if the sickness report can be closed off;
            // that can only happen if the sickness report was closed on
            // or before the cut-off date

            if (report.getLastDayOfSickness() != null &&
                    report.getLastDayOfSickness().compareTo(cutOffDate) > 0) {
                continue;
            }

            // Reassess the sickness for one last time and mark it as processed
            report.setHoursLost(getSicknessSpend(entityManager, report, null));
            report.setStatus(SicknessReportStatus.PROCESSED);

            // Merge changes to database
            entityManager.merge(report);
        }
    }
}